﻿using DevExpress.Data.Filtering;
using DevExpress.ExpressApp;
using DevExpress.ExpressApp.Data;
using DevExpress.ExpressApp.DC;
using DevExpress.Persistent.Base;
using MongoDB.Bson;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace EasyXaf.MongoDB;

[DomainComponent]
public abstract class MongoEntity : IObjectSpaceLink, INotifyPropertyChanged
{
    private ITypeInfo _typeInfo;
    private IObjectSpace _objectSpace;
    private readonly Dictionary<string, object> _propertyStore = [];

    protected IObjectSpace ObjectSpace
    {
        get => _objectSpace;
        set => _objectSpace = value?.Owner as IObjectSpace ?? value;
    }

    [Key]
    [VisibleInListView(false)]
    [VisibleInDetailView(false)]
    [VisibleInLookupListView(false)]
    public ObjectId Id { get; set; }

    public event PropertyChangedEventHandler PropertyChanged;

    public MongoEntity()
    {
        Id = ObjectId.GenerateNewId();
    }

    protected void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    protected ITypeInfo GetTypeInfo()
    {
        return _typeInfo ??= XafTypesInfo.Instance.FindTypeInfo(GetType());
    }

    protected IMemberInfo GetForeignKeyMemberInfo(string foreignKeyName, string propertyName)
    {
        if (string.IsNullOrWhiteSpace(foreignKeyName))
        {
            foreignKeyName = GetTypeInfo().FindMember(propertyName)?.FindAttribute<MongoForeignKeyAttribute>()?.ForeignKeyName;
        }

        return !string.IsNullOrWhiteSpace(foreignKeyName)
            ? GetTypeInfo().FindMember(foreignKeyName)
            : null;
    }

    protected T GetEntity<T>(string foreignKeyName = null, [CallerMemberName] string propertyName = null)
        where T : class
    {
        var foreignKeyMember = GetForeignKeyMemberInfo(foreignKeyName, propertyName)
            ?? throw new ArgumentException($"Foreign key member '{foreignKeyName}' not found in type '{GetType().Name}'.");

        T entityValue = null;
        var entityKey = foreignKeyMember.GetValue(this);

        if (entityKey == null)
        {
            _propertyStore[propertyName] = null;
            return null;
        }

        if (_propertyStore.TryGetValue(propertyName, out var cachedValue))
        {
            entityValue = cachedValue as T;
        }

        if (entityValue != null && entityKey != ObjectSpace.GetKeyValue(entityValue))
        {
            entityValue = null;
        }

        _propertyStore[propertyName] = (entityValue ??= ObjectSpace.GetObjectByKey<T>(entityKey));

        return entityValue;
    }

    protected bool SetEntity<T>(T newValue, string foreignKeyName = null, [CallerMemberName] string propertyName = null)
    {
        if (_propertyStore.TryGetValue(propertyName, out var cachedValue))
        {
            if (cachedValue is T oldValue && Equals(oldValue, newValue))
            {
                return false;
            }
        }

        var foreignKeyMember = GetForeignKeyMemberInfo(foreignKeyName, propertyName)
            ?? throw new ArgumentException($"Foreign key member '{foreignKeyName}' not found in type '{GetType().Name}'.");

        foreignKeyMember.SetValue(this, newValue == null ? null : ObjectSpace.GetKeyValue(newValue));

        _propertyStore[propertyName] = newValue;
        OnPropertyChanged(propertyName);

        return true;
    }

    protected MongoEntityCollection<T> GetEntityCollection<T>(string foreignKeyName = null, [CallerMemberName] string propertyName = null)
        where T : MongoEntity
    {
        if (!_propertyStore.TryGetValue(propertyName, out object value))
        {
            CriteriaOperator criteria = null;
            var memberInfo = GetTypeInfo().FindMember(propertyName);

            if (string.IsNullOrWhiteSpace(foreignKeyName))
            {
                var associationMemberInfo = memberInfo.AssociatedMemberInfo;
                foreignKeyName = associationMemberInfo?.FindAttribute<MongoForeignKeyAttribute>()?.ForeignKeyName;
            }

            if (!string.IsNullOrWhiteSpace(foreignKeyName) && Id != ObjectId.Empty)
            {
                criteria = new BinaryOperator(foreignKeyName, Id);
            }

            IList<T> list = criteria is null ? [] : [.. ObjectSpace.GetObjects<T>(criteria)];
            _propertyStore[propertyName] = value = new MongoEntityCollection<T>(list, this, memberInfo);
        }
        return (MongoEntityCollection<T>)value;
    }

    protected bool SetPropertyValue<T>(ref T propertyValue, T newValue, [CallerMemberName] string propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(propertyValue, newValue))
        {
            return false;
        }

        propertyValue = newValue;
        OnPropertyChanged(propertyName);

        return true;
    }

    public override int GetHashCode()
    {
        return Id.GetHashCode();
    }

    public override bool Equals(object obj)
    {
        if (obj is MongoEntity otherEntity)
        {
            return Equals(Id, otherEntity.Id);
        }
        return false;
    }

    IObjectSpace IObjectSpaceLink.ObjectSpace
    {
        get => ObjectSpace;
        set => ObjectSpace = value;
    }
}
